Skip to main content

MockMvc Spring Boot Testing Guide

Table of Contents

  1. Introduction to MockMvc
  2. Setting Up Test Dependencies
  3. Testing Controllers
  4. Testing Services
  5. Testing Repositories
  6. Integration Testing
  7. Testing Security
  8. Testing JSON Serialization/Deserialization
  9. Testing Exception Handling
  10. Advanced Testing Techniques
  11. Best Practices

Introduction to MockMvc

MockMvc is a powerful testing framework in Spring Boot that allows you to test Spring MVC controllers without starting a full HTTP server. It provides a fluent API for performing HTTP requests and validating responses.

Key Benefits

  • Fast execution - No need to start a web server
  • Isolated testing - Test specific layers independently
  • Comprehensive assertions - Rich API for validating responses
  • Mock integration - Easy integration with Mockito

Setting Up Test Dependencies

Maven Dependencies

<dependencies>
<!-- Spring Boot Test Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<!-- Spring Boot Web Starter (for MockMvc) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Spring Security Test (if using security) -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

Gradle Dependencies

dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.security:spring-security-test' // if using security
}

Testing Controllers

Basic Controller Test Setup

@WebMvcTest(UserController.class)
class UserControllerTest {

@Autowired
private MockMvc mockMvc;

@MockBean
private UserService userService;

@Test
void shouldGetUserById() throws Exception {
// Given
User user = new User(1L, "John Doe", "john@example.com");
when(userService.findById(1L)).thenReturn(user);

// When & Then
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("John Doe"))
.andExpect(jsonPath("$.email").value("john@example.com"));
}
}

Testing POST Requests

@Test
void shouldCreateUser() throws Exception {
// Given
User newUser = new User(null, "Jane Doe", "jane@example.com");
User savedUser = new User(2L, "Jane Doe", "jane@example.com");

when(userService.save(any(User.class))).thenReturn(savedUser);

// When & Then
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"name": "Jane Doe",
"email": "jane@example.com"
}
"""))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(2))
.andExpect(jsonPath("$.name").value("Jane Doe"));
}

Testing PUT Requests

@Test
void shouldUpdateUser() throws Exception {
// Given
User updatedUser = new User(1L, "John Smith", "johnsmith@example.com");
when(userService.update(eq(1L), any(User.class))).thenReturn(updatedUser);

// When & Then
mockMvc.perform(put("/api/users/1")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"name": "John Smith",
"email": "johnsmith@example.com"
}
"""))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("John Smith"));
}

Testing DELETE Requests

@Test
void shouldDeleteUser() throws Exception {
// Given
doNothing().when(userService).deleteById(1L);

// When & Then
mockMvc.perform(delete("/api/users/1"))
.andExpect(status().isNoContent());

verify(userService).deleteById(1L);
}

Testing Query Parameters

@Test
void shouldGetUsersWithPagination() throws Exception {
// Given
List<User> users = Arrays.asList(
new User(1L, "John", "john@example.com"),
new User(2L, "Jane", "jane@example.com")
);

when(userService.findAll(0, 10)).thenReturn(users);

// When & Then
mockMvc.perform(get("/api/users")
.param("page", "0")
.param("size", "10"))
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(2)))
.andExpect(jsonPath("$[0].name").value("John"))
.andExpect(jsonPath("$[1].name").value("Jane"));
}

Testing Request Headers

@Test
void shouldAcceptCustomHeader() throws Exception {
mockMvc.perform(get("/api/users/1")
.header("X-Request-ID", "12345")
.header("Accept", MediaType.APPLICATION_JSON_VALUE))
.andExpect(status().isOk());
}

Testing Services

Unit Testing Services with Mockito

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

@Mock
private UserRepository userRepository;

@Mock
private EmailService emailService;

@InjectMocks
private UserService userService;

@Test
void shouldFindUserById() {
// Given
User user = new User(1L, "John Doe", "john@example.com");
when(userRepository.findById(1L)).thenReturn(Optional.of(user));

// When
User result = userService.findById(1L);

// Then
assertThat(result).isNotNull();
assertThat(result.getName()).isEqualTo("John Doe");
verify(userRepository).findById(1L);
}

@Test
void shouldThrowExceptionWhenUserNotFound() {
// Given
when(userRepository.findById(1L)).thenReturn(Optional.empty());

// When & Then
assertThatThrownBy(() -> userService.findById(1L))
.isInstanceOf(UserNotFoundException.class)
.hasMessage("User not found with id: 1");
}

@Test
void shouldSaveUserAndSendWelcomeEmail() {
// Given
User user = new User(null, "John Doe", "john@example.com");
User savedUser = new User(1L, "John Doe", "john@example.com");

when(userRepository.save(user)).thenReturn(savedUser);

// When
User result = userService.save(user);

// Then
assertThat(result.getId()).isEqualTo(1L);
verify(userRepository).save(user);
verify(emailService).sendWelcomeEmail("john@example.com");
}
}

Testing Service with @MockBean in Spring Context

@SpringBootTest
class UserServiceIntegrationTest {

@MockBean
private UserRepository userRepository;

@Autowired
private UserService userService;

@Test
void shouldFindAllUsers() {
// Given
List<User> users = Arrays.asList(
new User(1L, "John", "john@example.com"),
new User(2L, "Jane", "jane@example.com")
);
when(userRepository.findAll()).thenReturn(users);

// When
List<User> result = userService.findAll();

// Then
assertThat(result).hasSize(2);
assertThat(result.get(0).getName()).isEqualTo("John");
}
}

Testing Repositories

Testing JPA Repositories with @DataJpaTest

@DataJpaTest
class UserRepositoryTest {

@Autowired
private TestEntityManager entityManager;

@Autowired
private UserRepository userRepository;

@Test
void shouldFindUserByEmail() {
// Given
User user = new User(null, "John Doe", "john@example.com");
entityManager.persistAndFlush(user);

// When
Optional<User> result = userRepository.findByEmail("john@example.com");

// Then
assertThat(result).isPresent();
assertThat(result.get().getName()).isEqualTo("John Doe");
}

@Test
void shouldFindUsersByNameContaining() {
// Given
User user1 = new User(null, "John Doe", "john@example.com");
User user2 = new User(null, "Jane Doe", "jane@example.com");
User user3 = new User(null, "Bob Smith", "bob@example.com");

entityManager.persistAndFlush(user1);
entityManager.persistAndFlush(user2);
entityManager.persistAndFlush(user3);

// When
List<User> result = userRepository.findByNameContaining("Doe");

// Then
assertThat(result).hasSize(2);
assertThat(result).extracting(User::getName)
.containsExactlyInAnyOrder("John Doe", "Jane Doe");
}

@Test
void shouldDeleteUserById() {
// Given
User user = new User(null, "John Doe", "john@example.com");
User savedUser = entityManager.persistAndFlush(user);

// When
userRepository.deleteById(savedUser.getId());

// Then
Optional<User> result = userRepository.findById(savedUser.getId());
assertThat(result).isEmpty();
}
}

Testing Custom Repository Methods

@DataJpaTest
class UserRepositoryCustomTest {

@Autowired
private UserRepository userRepository;

@Test
@Sql("/test-data.sql") // Load test data from SQL file
void shouldFindActiveUsersCreatedAfterDate() {
// Given
LocalDateTime cutoffDate = LocalDateTime.of(2023, 1, 1, 0, 0);

// When
List<User> result = userRepository.findActiveUsersCreatedAfter(cutoffDate);

// Then
assertThat(result).isNotEmpty();
assertThat(result).allMatch(user ->
user.isActive() && user.getCreatedAt().isAfter(cutoffDate));
}
}

Integration Testing

Full Integration Test with @SpringBootTest

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
class UserIntegrationTest {

@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:13")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");

@Autowired
private TestRestTemplate restTemplate;

@Autowired
private UserRepository userRepository;

@Test
void shouldCreateAndRetrieveUser() {
// Given
User user = new User(null, "Integration Test User", "integration@example.com");

// When - Create user
ResponseEntity<User> createResponse = restTemplate.postForEntity(
"/api/users", user, User.class);

// Then - Verify creation
assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(createResponse.getBody()).isNotNull();
Long userId = createResponse.getBody().getId();

// When - Retrieve user
ResponseEntity<User> getResponse = restTemplate.getForEntity(
"/api/users/" + userId, User.class);

// Then - Verify retrieval
assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(getResponse.getBody().getName()).isEqualTo("Integration Test User");

// Verify in database
Optional<User> dbUser = userRepository.findById(userId);
assertThat(dbUser).isPresent();
}
}

Testing with MockMvc and Real Database

@SpringBootTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@AutoConfigureMockMvc
@Transactional
class UserControllerIntegrationTest {

@Autowired
private MockMvc mockMvc;

@Autowired
private UserRepository userRepository;

@Test
void shouldCreateUserInDatabase() throws Exception {
// When
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"name": "Database Test User",
"email": "dbtest@example.com"
}
"""))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").exists());

// Then - Verify in database
Optional<User> user = userRepository.findByEmail("dbtest@example.com");
assertThat(user).isPresent();
assertThat(user.get().getName()).isEqualTo("Database Test User");
}
}

Testing Security

Testing Secured Endpoints

@WebMvcTest(UserController.class)
@Import(SecurityConfig.class)
class UserControllerSecurityTest {

@Autowired
private MockMvc mockMvc;

@MockBean
private UserService userService;

@Test
void shouldReturnUnauthorizedWhenNoToken() throws Exception {
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isUnauthorized());
}

@Test
@WithMockUser(roles = "USER")
void shouldAllowUserRoleAccess() throws Exception {
User user = new User(1L, "John", "john@example.com");
when(userService.findById(1L)).thenReturn(user);

mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk());
}

@Test
@WithMockUser(roles = "ADMIN")
void shouldAllowAdminToDeleteUser() throws Exception {
doNothing().when(userService).deleteById(1L);

mockMvc.perform(delete("/api/users/1"))
.andExpect(status().isNoContent());
}

@Test
@WithMockUser(roles = "USER")
void shouldForbidUserFromDeletingUser() throws Exception {
mockMvc.perform(delete("/api/users/1"))
.andExpect(status().isForbidden());
}
}

Testing JWT Authentication

@Test
void shouldAuthenticateWithValidJWT() throws Exception {
String token = jwtTokenProvider.createToken("john@example.com", Arrays.asList("ROLE_USER"));

mockMvc.perform(get("/api/users/profile")
.header("Authorization", "Bearer " + token))
.andExpect(status().isOk());
}

Testing JSON Serialization/Deserialization

Testing JSON Serialization with @JsonTest

@JsonTest
class UserJsonTest {

@Autowired
private JacksonTester<User> json;

@Test
void shouldSerializeUser() throws Exception {
User user = new User(1L, "John Doe", "john@example.com");

assertThat(json.write(user)).isEqualToJson("expected-user.json");
assertThat(json.write(user)).hasJsonPathStringValue("@.name");
assertThat(json.write(user)).extractingJsonPathStringValue("@.name")
.isEqualTo("John Doe");
}

@Test
void shouldDeserializeUser() throws Exception {
String jsonContent = """
{
"id": 1,
"name": "John Doe",
"email": "john@example.com"
}
""";

assertThat(json.parse(jsonContent))
.usingRecursiveComparison()
.isEqualTo(new User(1L, "John Doe", "john@example.com"));
}
}

Testing Exception Handling

Testing Global Exception Handler

@WebMvcTest(UserController.class)
class UserControllerExceptionTest {

@Autowired
private MockMvc mockMvc;

@MockBean
private UserService userService;

@Test
void shouldHandleUserNotFoundException() throws Exception {
when(userService.findById(999L))
.thenThrow(new UserNotFoundException("User not found with id: 999"));

mockMvc.perform(get("/api/users/999"))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.message").value("User not found with id: 999"))
.andExpect(jsonPath("$.timestamp").exists());
}

@Test
void shouldHandleValidationErrors() throws Exception {
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"name": "",
"email": "invalid-email"
}
"""))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.validationErrors").exists())
.andExpect(jsonPath("$.validationErrors.name").value("Name is required"))
.andExpect(jsonPath("$.validationErrors.email").value("Email format is invalid"));
}
}

Advanced Testing Techniques

Testing File Uploads

@Test
void shouldUploadUserProfilePicture() throws Exception {
MockMultipartFile file = new MockMultipartFile(
"file",
"profile.jpg",
MediaType.IMAGE_JPEG_VALUE,
"image content".getBytes());

mockMvc.perform(multipart("/api/users/1/profile-picture")
.file(file))
.andExpect(status().isOk())
.andExpect(jsonPath("$.message").value("Profile picture uploaded successfully"));
}

Testing Async Operations

@Test
void shouldProcessAsyncUserRegistration() throws Exception {
when(userService.registerAsync(any(User.class)))
.thenReturn(CompletableFuture.completedFuture("Registration completed"));

MvcResult result = mockMvc.perform(post("/api/users/register-async")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"name": "Async User",
"email": "async@example.com"
}
"""))
.andExpect(request().asyncStarted())
.andReturn();

mockMvc.perform(asyncDispatch(result))
.andExpect(status().isOk())
.andExpect(content().string("Registration completed"));
}

Testing with Custom Argument Resolvers

@WebMvcTest(UserController.class)
@Import(CustomArgumentResolverConfig.class)
class UserControllerCustomResolverTest {

@Autowired
private MockMvc mockMvc;

@Test
void shouldResolveCurrentUserFromToken() throws Exception {
mockMvc.perform(get("/api/users/current")
.header("Authorization", "Bearer valid-jwt-token"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Current User"));
}
}

Testing Caching

@SpringBootTest
@AutoConfigureMockMvc
class UserCachingTest {

@Autowired
private MockMvc mockMvc;

@MockBean
private UserRepository userRepository;

@Test
void shouldCacheUserData() throws Exception {
User user = new User(1L, "John", "john@example.com");
when(userRepository.findById(1L)).thenReturn(Optional.of(user));

// First call - should hit the repository
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk());

// Second call - should use cache
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk());

// Verify repository was called only once
verify(userRepository, times(1)).findById(1L);
}
}

Best Practices

1. Test Organization

@DisplayName("User Controller Tests")
class UserControllerTest {

@Nested
@DisplayName("GET /api/users/{id}")
class GetUserById {

@Test
@DisplayName("Should return user when valid ID provided")
void shouldReturnUser_WhenValidIdProvided() {
// Test implementation
}

@Test
@DisplayName("Should return 404 when user not found")
void shouldReturn404_WhenUserNotFound() {
// Test implementation
}
}

@Nested
@DisplayName("POST /api/users")
class CreateUser {

@Test
@DisplayName("Should create user with valid data")
void shouldCreateUser_WithValidData() {
// Test implementation
}
}
}

2. Test Data Builders

public class UserTestDataBuilder {
private Long id = 1L;
private String name = "John Doe";
private String email = "john@example.com";
private boolean active = true;

public static UserTestDataBuilder aUser() {
return new UserTestDataBuilder();
}

public UserTestDataBuilder withId(Long id) {
this.id = id;
return this;
}

public UserTestDataBuilder withName(String name) {
this.name = name;
return this;
}

public UserTestDataBuilder withEmail(String email) {
this.email = email;
return this;
}

public UserTestDataBuilder inactive() {
this.active = false;
return this;
}

public User build() {
return new User(id, name, email, active);
}
}

// Usage in tests
@Test
void shouldCreateInactiveUser() {
User user = aUser().withName("Jane").inactive().build();
// Test with user
}

3. Custom Matchers

public class UserMatchers {

public static Matcher<User> hasName(String expectedName) {
return new TypeSafeMatcher<User>() {
@Override
protected boolean matchesSafely(User user) {
return Objects.equals(user.getName(), expectedName);
}

@Override
public void describeTo(Description description) {
description.appendText("user with name ").appendValue(expectedName);
}
};
}
}

// Usage
assertThat(user, hasName("John Doe"));

4. Test Configuration

@TestConfiguration
public class TestConfig {

@Bean
@Primary
public Clock testClock() {
return Clock.fixed(Instant.parse("2023-01-01T00:00:00Z"), ZoneOffset.UTC);
}

@Bean
@Primary
public EmailService mockEmailService() {
return Mockito.mock(EmailService.class);
}
}

5. Test Profiles

# application-test.yml
spring:
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create-drop
logging:
level:
org.springframework.web: DEBUG

6. Parameterized Tests

@ParameterizedTest
@ValueSource(strings = {"", " ", "invalid-email", "@example.com"})
void shouldRejectInvalidEmails(String invalidEmail) throws Exception {
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(String.format("""
{
"name": "Test User",
"email": "%s"
}
""", invalidEmail)))
.andExpect(status().isBadRequest());
}

@ParameterizedTest
@CsvSource({
"1, John, john@example.com",
"2, Jane, jane@example.com",
"3, Bob, bob@example.com"
})
void shouldReturnUsersWithValidData(Long id, String name, String email) throws Exception {
User user = new User(id, name, email);
when(userService.findById(id)).thenReturn(user);

mockMvc.perform(get("/api/users/" + id))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value(name))
.andExpect(jsonPath("$.email").value(email));
}

Summary

This guide covers comprehensive testing strategies for Spring Boot applications using MockMvc:

  • Controller Testing: Unit tests with mocked dependencies
  • Service Testing: Business logic validation with mocked repositories
  • Repository Testing: Data access layer testing with @DataJpaTest
  • Integration Testing: End-to-end testing with real databases
  • Security Testing: Authentication and authorization testing
  • Advanced Techniques: File uploads, async operations, caching

Remember to:

  • Use appropriate test slices (@WebMvcTest, @DataJpaTest, @JsonTest)
  • Mock external dependencies appropriately
  • Write clear, descriptive test names
  • Organize tests logically with @Nested classes
  • Use test data builders for complex objects
  • Follow the AAA pattern (Arrange, Act, Assert)
  • Keep tests independent and repeatable